/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the authors at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/
package com.ericdaugherty.mail.server.configuration;

//Java Imports
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.*;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//Local imports
import com.ericdaugherty.mail.server.Mail;
import com.ericdaugherty.mail.server.configuration.backEnd.PersistExecutor;
import com.ericdaugherty.mail.server.configuration.cbc.*;

/**
 * Add a user and optionally an association with a (new) realm using
 * a custom communication protocol over a tcp connection.
 *
 * @author Andreas Kyrmegalos
 */
public class ConnectionBasedConfigurator implements Runnable, ConnectionBasedConfiguratorConstants {

   /** Logger Category for this class. */
   private static final Log log = LogFactory.getLog(ConnectionBasedConfigurator.class);
   /** The server socket used to listen for incoming connections */
   private ServerSocket serverSocket;

    private static SSLServerSocketFactory sslServerSocketFactory;

    static {
       SSLContext sslContext = ConfigurationManager.getInstance().getSSLContext();
       if (sslContext!=null) {
          sslServerSocketFactory = sslContext.getServerSocketFactory();
       }
    }
   /** Socket connection to the client */
   private Socket socket;
   /** Reader to read data from the client */
   private BufferedReader in;
   private final ConfigurationManager cm = ConfigurationManager.getInstance();
   private volatile boolean shutdown;
   private final ThreadFactory tf;
   private final ExecutorService es;

   final class ExecutorThreadFactory implements ThreadFactory {

      private AtomicInteger ai = new AtomicInteger(1);
      private ThreadGroup tg;

      public ExecutorThreadFactory() {
         tg = new ThreadGroup("CBCGroup");
         tg.setMaxPriority(Thread.currentThread().getPriority() - 1);
         tg.setDaemon(false);
      }

      public Thread newThread(Runnable runnable) {
         return new Thread(tg, runnable, tg.getName() + "-" + ai.getAndIncrement());
      }
   }

   final class CallableCBC<T> implements Callable<T> {

      final Runnable task;
      final T result;

      CallableCBC(Runnable task, T result) {
         this.task = task;
         this.result = result;
      }

      public T call() {
         task.run();
         return result;
      }
   }
   private AccessControlContext acc;

   public ConnectionBasedConfigurator(boolean secure) {

      PermissionCollection pc = new Permissions();
      InetAddress listenAddress = cm.getConfigurationAddress();
      if (listenAddress == null) {
         throw new RuntimeException("The CBC address can not be zero based");
      }
      pc.add(new SocketPermission(listenAddress.getHostAddress() + ":" + cm.getConfigurationPort(), "listen,resolve"));
      if (!secure) {
         pc.add(new SocketPermission(listenAddress.getHostAddress() + ":*", "accept,resolve"));
      }
      else {
         pc.add(new SocketPermission("*", "accept,resolve"));
      }
      acc = new AccessControlContext(new ProtectionDomain[]{new ProtectionDomain(null, pc)});

      tf = new ExecutorThreadFactory();
      es = Executors.newSingleThreadExecutor(tf);

      try {

         acc.checkPermission(new SocketPermission(listenAddress.getHostAddress() + ":" + cm.getConfigurationPort(), "listen,resolve"));
         if (!secure) {
            serverSocket = new ServerSocket(cm.getConfigurationPort(), 5, listenAddress);
         }
         else {
            serverSocket = sslServerSocketFactory.createServerSocket(cm.getConfigurationPort(), 5, listenAddress);
            ((SSLServerSocket)serverSocket).
                     setEnabledCipherSuites(ConfigurationManager.getInstance().getEnabledCiphers());
            ((SSLServerSocket)serverSocket).
                     setEnabledProtocols(ConfigurationManager.ENABLED_PROTOCOLS);
            ((SSLServerSocket)serverSocket).setNeedClientAuth(true);
         }

         log.info("CBC will be listening for changes.");
      } catch (IOException e) {

         log.error("Could not initiate ConnectionBasedConfigurator");
         Mail.getInstance().shutdown();
      }
   }

   public void shutdown() {
      log.warn("Remote Configurator going offline");
      shutdown = true;
      if (serverSocket != null) {
         try {
            serverSocket.close();
         } catch (IOException ex) {
         }
      }
      if (socket != null) {
         try {
            socket.close();
         } catch (IOException ex) {
         }
      }
      try {
         AccessController.doPrivileged(new PrivilegedExceptionAction<List<Runnable>>() {

            public List<Runnable> run() {
               return es.shutdownNow();
            }
         });
      } catch (PrivilegedActionException ex) {
         log.error(ex);
      }
   }

   public void run() {
      try {
         //Set the socket to timeout every 10 seconds so it does not
         //just block forever.
         serverSocket.setSoTimeout(10 * 1000);
      } catch (SocketException e) {
         log.fatal("Error initializing Socket Timeout in ConnectionBasedConfigurator");
         throw new RuntimeException();
      }

      List<String> lines;
      boolean timedOut;
      do {

         lines = null;
         timedOut = false;
         try {
            socket = serverSocket.accept();
            acc.checkPermission(new SocketPermission(socket.getInetAddress().getHostAddress() + ":" + socket.getPort(), "accept,resolve"));
            socket.setSoTimeout(2 * 60 * 1000);
            log.info("Configuration Connection Established.");

            ((SSLSocket)socket).getSession().invalidate();

            in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));

            lines = readInput();
            if (lines != null) {
               processLines(lines);
            }

         } catch (java.net.SocketTimeoutException e) {
            timedOut = true;
         } catch (Exception e) {
            log.error(e.getMessage());
         } finally {
            if (!timedOut) {
               log.info("Configuration Connection Closing.");
            }
            if (socket != null) {
               try {
                  socket.close();
               } catch (IOException ex) {
               }
               socket = null;
            }
         }
      } while (!shutdown);
   }

   private void processLines(List<String> lines) {

      String line;
      ListIterator<String> iter = lines.listIterator();
      BackEndTypeEnum backEndType = cm.getBackEndType();
      CBCExecutor cbcExecutor = null;
      while (iter.hasNext()) {
         line = iter.next();
         if (backEndType == BackEndTypeEnum.FILE) {

            cbcExecutor = new AddUsersPLL1(iter);
            cbcExecutor.processLines();
         } else if (backEndType == BackEndTypeEnum.RDBM) {
            if (line.startsWith(COMMAND_INSERT_DOMAIN)) {

               cbcExecutor = new InsertDomainPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_DELETE_DOMAIN)) {

               cbcExecutor = new DeleteDomainPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_SET_DEFAULT_DOMAIN)) {

               cbcExecutor = new SetDefaultDomainPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_INSERT_USER)) {

               cbcExecutor = new InsertUserPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_DELETE_USER)) {

               cbcExecutor = new DeleteUserPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_SET_USER_PASSWORD)) {

               cbcExecutor = new SetUserPasswordPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_ADD_FORWARD_ADDRESS)) {

               cbcExecutor = new AddForwardAddressPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_REMOVE_FORWARD_ADDRESS)) {

               cbcExecutor = new RemoveForwardAddressPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_SET_DEFAULT_MAILBOX)) {

               cbcExecutor = new SetDefaultMailboxPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_INSERT_REALM)) {

               cbcExecutor = new InsertRealmPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_DELETE_REALM)) {

               cbcExecutor = new RemoveRealmPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_ADD_USER_TO_REALM)) {

               cbcExecutor = new AddUserToRealmPLL1(iter);
               cbcExecutor.processLines();
            } else if (line.startsWith(COMMAND_REMOVE_USER_FROM_REALM)) {

               cbcExecutor = new RemoveUserFromRealmPLL1(iter);
               cbcExecutor.processLines();
            } //Reject anything else
            else {
               //TODO should we return anything???
            }
         }

      }

      es.submit(new CallableCBC<RunnableCBC>((new RunnableCBC(cbcExecutor)), null));
   }

   private final class RunnableCBC implements Runnable {

      private CBCExecutor cbcExecutor;

      public RunnableCBC(CBCExecutor cbcExecutor) {
         this.cbcExecutor = cbcExecutor;
      }

      public void run() {
         cm.updateThroughConnection(cbcExecutor);
      }
   }

   public final class AddUsersPLL1 extends CBCExecutor {

      private final List<NewUser> newUsers = new ArrayList<NewUser>(10);

      public AddUsersPLL1(ListIterator<String> iter) {
         super(iter);
      }

      public void processLines() {
         String line;
         for (; iter.hasNext();) {
            line = iter.next();
            if (line.toLowerCase().startsWith(COMMAND_ADD_USER)) {
               NewUser newUser = new NewUser();
               for (; iter.hasNext();) {
                  line = iter.next();
                  if (line.toLowerCase().startsWith(USERNAME)) {
                     newUser.username = line.substring(USERNAME.length()).trim();
                  } else if (line.toLowerCase().startsWith(PASSWORD)) {
                     newUser.password = line.substring(PASSWORD.length()).trim();
                  } else if (line.toLowerCase().startsWith(REALM)) {
                     newUser.realm = line.substring(REALM.length()).trim();
                  } else {
                     if (iter.hasNext()) {
                        iter.previous();
                     }
                     break;
                  }
               }
               if (newUser.username != null && newUser.password != null) {
                  newUsers.add(newUser);
               }
            } //Reject anything else
            else {
               //TODO should we return anything???
            }
         }
      }

      public List<NewUser> getNewUsers() {
         return newUsers;
      }

      public void execute(PersistExecutor pe) {
      }
   }

   private List<String> readInput() throws IOException, NullPointerException {
      String line;
      List<String> inputLines = new ArrayList<String>();
      int lineCount = 0;
      for (;;) {
         line = in.readLine().trim();
         if (line.equals(".")) {
            break;
         }
         if (line.toLowerCase().indexOf("password") == -1) {
            log.info(line);
         }
         inputLines.add(line);
         lineCount++;
         if (lineCount == 100) {
            break;
         }
      }
      log.info("New configuration parameters received.");
      return inputLines;
   }
}
